package vsa.migrations.junit.migrators;

import java.util.HashMap;
import java.util.logging.Logger;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.emftext.commons.layout.LayoutFactory;
import org.emftext.commons.layout.LayoutInformation;
import org.emftext.language.java.annotations.AnnotationInstance;
import org.emftext.language.java.annotations.AnnotationsFactory;
import org.emftext.language.java.annotations.SingleAnnotationParameter;
import org.emftext.language.java.classifiers.Class;
import org.emftext.language.java.classifiers.ConcreteClassifier;
import org.emftext.language.java.containers.CompilationUnit;
import org.emftext.language.java.expressions.Expression;
import org.emftext.language.java.members.ClassMethod;
import org.emftext.language.java.members.Constructor;
import org.emftext.language.java.members.MembersFactory;
import org.emftext.language.java.members.Method;
import org.emftext.language.java.references.IdentifierReference;
import org.emftext.language.java.references.MethodCall;
import org.emftext.language.java.references.ReferencesFactory;
import org.emftext.language.java.references.ReflectiveClassReference;
import org.emftext.language.java.statements.ExpressionStatement;

import vsa.migrations.junit.JunitMigrationException;
import vsa.migrations.junit.MigrationUtil;

/**
 * migrates one compileunit to junit4
 * supports even classes, which were migrated to junit4 before
 * @author oleym
 *
 */
public class GenericJunitClassMigrator extends AbstractJunitMigrator implements JunitMigrator {

	private static Logger LOGGER = Logger.getLogger(GenericJunitClassMigrator.class.getName());


	/**
	 * cleanups the migrated classes
	 * - removes old imports
	 * - removes direct superclass-reference
	 * @param compilationunit
	 * @param clazz
	 * @throws JunitMigrationException
	 */
	public void cleanupTestCase(CompilationUnit compilationunit, Class clazz) throws JunitMigrationException{

	  ConcreteClassifier classifierTestCaseJunit3 = migrationutil.getClassifierTestCaseJunit3(compilationunit);
	  ConcreteClassifier classifierObject = migrationutil.getClassifierObject(compilationunit);
	  //Remove all junit.framework imports
	  migrationutil.removeImports(compilationunit, MigrationUtil.PACKAGE_JUNIT3);

	  //Remove all super-calls and remove direct super class TestCase afterwards
	  if (clazz.getSuperClass().equals(classifierTestCaseJunit3)) {
	  	migrationutil.removeSuperCalls(clazz);
	  	clazz.setDefaultExtends(clazz.getExtends());
	  	((org.emftext.language.java.types.ClassifierReference)clazz.getDefaultExtends().getPureClassifierReference()).setTarget(classifierObject);
	  	clazz.setExtends(null);
	  }

	  if (migrationutil.isConcreteTestclass(clazz)) {
	    for (Constructor constructor : migrationutil.getConstructors(clazz)) {
	    	if (migrationutil.callsOnlySuperDefaultConstructor(constructor))
  	  		  EcoreUtil.remove(constructor);
	    }
	  }

	  //Comment suite and main - methods
	  for (Method nextMethod: clazz.getMethods()) {
		  if (nextMethod.isStatic() && nextMethod.getName().equals("suite")) {
		  		migrationutil.comment(clazz, nextMethod, "TODO suite-method not allowed in junit4, use Suite-runner for creating a suite for some testclasses");
		  	}

		  	if (nextMethod.isStatic() && nextMethod.getName().equals("main")) {
		  		migrationutil.comment(clazz, nextMethod, "TODO main-method is not needed in testclasses");
		  	}
	  }

	  //Remove baseclass if class was previously mapped with a runner
	  HashMap<String,String> runnerMapping = getConfigurator().getRunnerMapping();
	  for (String abstractTestCaseFQN : runnerMapping.keySet()) {
		  	Class nextBaseClass = (Class) migrationutil.getClassifier(compilationunit, abstractTestCaseFQN);
		  	if (migrationutil.extendsClass(clazz, nextBaseClass)) {
		  		ConcreteClassifier classifierObject2 = migrationutil.getClassifierObject(compilationunit);
		  	  	clazz.setDefaultExtends(clazz.getExtends());
		  	  	((org.emftext.language.java.types.ClassifierReference)clazz.getDefaultExtends().getPureClassifierReference()).setTarget(classifierObject2);
		  	  	clazz.setExtends(null);
		  	}
	  }


	  //Remove setUp and tearDown if they are empty
	  Method setupEmpty = migrationutil.findMethod(clazz, METHODENAME_SETUP);
	  if (setupEmpty != null && migrationutil.isEmpty(setupEmpty))
	  	  EcoreUtil.remove(setupEmpty);

	  Method teardownEmpty = migrationutil.findMethod(clazz, METHODENAME_TEARDOWN);
	  if (teardownEmpty != null && migrationutil.isEmpty(teardownEmpty))
	  	  EcoreUtil.remove(teardownEmpty);

	}

	/**
	 * migrates one testcase
	 * @param compilationunit current compilationunit
	 * @param clazz  current class
	 * @throws JunitMigrationException exception if error while migration is thrown
	 */
	public void migrateTestCase(CompilationUnit compilationunit, Class clazz) throws JunitMigrationException{
	  LOGGER.info("Migrate " + compilationunit.getName() + " to Junit4");


	  defaultConstructorInSuperClasses(compilationunit, clazz);
	  addAnnotations(compilationunit, clazz);
	  replaceAsserts(compilationunit, clazz);

	  mapAbstractTestCase(compilationunit, clazz);

	  migrationutil.logInfo (compilationunit, "Migrated " + compilationunit.getName() + " to Junit4\n");
	}

	/**
	 * replace the Asssert.foo(...) by static imports with foo(...)
	 * @param compilationunit current compilationunit
	 * @param clazz  current class
	 */
	private void replaceAsserts (CompilationUnit compilationunit, Class clazz) {
	  ConcreteClassifier classifierAssertJunit4 = migrationutil.getClassifier(compilationunit, getConfigurator().getUsedAssertClass());
	  for (Method nextMethod: clazz.getMethods()) {

	  	//Add static imports for Assert-methodcalls and replace Assert by direct method-call
	  	EList<Expression> childrenByType = nextMethod.getChildrenByType(Expression.class);
	  	for (Expression nextExpression: childrenByType)  {
	  		if (nextExpression instanceof MethodCall) {
	  			MethodCall methodCall = (MethodCall) nextExpression;

	  			if (methodCall.getTarget() instanceof ClassMethod) {
	  				ClassMethod classmethod = (ClassMethod) methodCall.getTarget();
	  				if (classmethod.getName().startsWith("assert") && classmethod.getContainingConcreteClassifier().getName().equals("Assert") ) {
	  					migrationutil.addStaticImportOnDemand(compilationunit, classifierAssertJunit4, classmethod);
	  					ExpressionStatement containingStatement = methodCall.getParentByType(ExpressionStatement.class);
	  					containingStatement.setExpression(methodCall);
	  				}
	  			}
	  		}
	  	}

	  }
	}

	/**
	 * add annotations to testmethods, setUp, tearDown
	 * @param compilationunit current compilationunit
	 * @param clazz  current class
	 */
	private void addAnnotations (CompilationUnit compilationunit, Class clazz) {
	  ConcreteClassifier classifierTest = migrationutil.getClassifierTest(compilationunit);
	  ConcreteClassifier classifierBefore = migrationutil.getClassifierBefore(compilationunit);
	  ConcreteClassifier classifierAfter = migrationutil.getClassifierAfter(compilationunit);

	  for (Method nextMethod: clazz.getMethods()) {

	  	//annotate methods, which are not static, returning void and start with test with @Test
	  	if (migrationutil.isTestMethod(nextMethod)) {
	  		migrationutil.logInfo (compilationunit, "  -  Add @Test to " + nextMethod.getName());
	  		migrationutil.addAnnotationOnDemand(nextMethod, classifierTest);
	  	}

	  	//annotate protected or public void method setUp() with @Before and make it public
	  	if (nextMethod.getName().equals(METHODENAME_SETUP) && (nextMethod.isProtected() || nextMethod.isPublic())) {
	  		migrationutil.logInfo (compilationunit, "  -  Add @Before to " + nextMethod.getName() + " and make method public");
	  		migrationutil.addAnnotationOnDemand(nextMethod, classifierBefore);
	  	}

	  	//annotate protected or public void method tearDown() with @After and make it public
	  	if (nextMethod.getName().equals(METHODENAME_TEARDOWN) && (nextMethod.isProtected() || nextMethod.isPublic())) {
	  		migrationutil.logInfo (compilationunit, "  -  Add @After to " + nextMethod.getName() + " and make method public");
	  		migrationutil.addAnnotationOnDemand(nextMethod, classifierAfter);
	  	}

	  	migrationutil.normalizelayout (nextMethod);


	  }
	}

	/**
	 * in superclasses we add a default-constructor or make an existing default-constructor public
	 * @param compilationunit
	 * @param clazz
	 */
	private void defaultConstructorInSuperClasses (CompilationUnit compilationunit, Class clazz) {
	  if (! migrationutil.isConcreteTestclass(clazz)) {
	    Constructor defaultConstructor = migrationutil.getDefaultConstructor(clazz, false);
	    if (defaultConstructor != null) { //make a default-constructor public if not already done
	      if (! defaultConstructor.isPublic())
	    	  defaultConstructor.makePublic();
	    }
	    else {
	  	//if non-default-constructors are available and no default-constructor is available
	  	//we have to add a default-constructor to make test runnable because junit4 forces it
	  	if (migrationutil.getConstructors(clazz).size() > 0) {
	  	  Constructor emptyDefaultConstructor = MembersFactory.eINSTANCE.createConstructor();
	  	  emptyDefaultConstructor.makePublic();
	  	  emptyDefaultConstructor.setName(clazz.getName());
	  	  LayoutInformation layoutInfo = LayoutFactory.eINSTANCE.createAttributeLayoutInformation();
	  	  layoutInfo.setHiddenTokenText("Default-constructor generated by junit4-migration automatically");
	  	  emptyDefaultConstructor.getLayoutInformations().add(layoutInfo);
	  	  clazz.getMembers().add(0, emptyDefaultConstructor);
	  	}
	    }
	  }

	}

	/**
	 * maps abstract base classes to runners, annotated with @RunWith
	 * if no Runner was defined before
	 * @param compilationunit  current compilation unit
	 * @param clazz  current class
	 * @throws JunitMigrationException exception
	 */
	private void mapAbstractTestCase (CompilationUnit compilationunit, Class clazz) throws JunitMigrationException {
		
	  //if RunWith is already defined we don't want to override it
	  ConcreteClassifier classifierRunwith = migrationutil.getClassifierRunwith(compilationunit);
	  AnnotationInstance findAnnotation = migrationutil.findAnnotation(clazz, classifierRunwith);
  	  if (findAnnotation != null) 
        return;
  	  
  	  //and abstract class should not get any RunWith, too
  	  if (migrationutil.isAbstract(clazz))
  		  return; 
  	  
  	  //and if we have an abstract class we don't want a RunWith, too

	  HashMap<String,String> runnerMapping = getConfigurator().getRunnerMapping();
	  for (String abstractTestCaseFQN : runnerMapping.keySet()) {
	  	Class nextBaseClass = (Class) migrationutil.getClassifier(compilationunit, abstractTestCaseFQN);
	  	if (migrationutil.extendsClass(clazz, nextBaseClass)) {
	  		String runnerFQN = runnerMapping.get(abstractTestCaseFQN);
	  		Class runnerClass = (Class) migrationutil.getClassifier(compilationunit, runnerFQN);
	  		ConcreteClassifier classifierRunWith = migrationutil.getClassifierRunwith(compilationunit);
	  		AnnotationInstance runWithAnnotation = migrationutil.addAnnotationOnDemand(clazz, classifierRunWith);
	  		SingleAnnotationParameter param = AnnotationsFactory.eINSTANCE.createSingleAnnotationParameter();

	  		IdentifierReference identifierReference = ReferencesFactory.eINSTANCE.createIdentifierReference();
	  		identifierReference.setTarget(runnerClass);

	  		ReflectiveClassReference reflectiveClassReference = ReferencesFactory.eINSTANCE.createReflectiveClassReference();
	  		identifierReference.setNext(reflectiveClassReference);

	  		migrationutil.addImportOnDemand(compilationunit, runnerClass);

	  		param.setValue(identifierReference);
	  		runWithAnnotation.setParameter(param);


	  		break;
	  	}
	  }
	}
}
